Julia数据科学系列-Queryverse系列包

Queryverse.jl系列工具 Queryverse是一系列数据处理工具包集合, 目前包含如下工具:

  • Query.jl: 数据处理

  • VegaLite.jlVega.jl: Vega画图语法的julia封装

  • DataVoyager.jl: Vega的Voyager的julia封装

  • ElectronDisplay.jl: 基于Electron的可视化(似乎对我不太有用)

官方提供了一个Queryverse.jl包, 基本包含了常用的Julia数据分析相关包:

julia

module Queryverse

using Reexport

@reexport using DataValues
import IterableTables
@reexport using Query
@reexport using DataTables
@reexport using DataFrames
@reexport using FileIO
@reexport using ExcelFiles
@reexport using StatFiles
@reexport using CSVFiles
@reexport using FeatherFiles
@reexport using ParquetFiles
@reexport using VegaLite
@reexport using DataVoyager

export DV

export @tee

macro tee(ex)
    quote
        x -> begin
            x |> $(esc(ex))
            return x
        end
    end
end

const DV = DataValues.DataValue

end # module

julia

Query.jl

基本上可以对Julia中绝大部分可迭代数据类型(支持TableTraits.jl的数据类型, 涵盖了所有IterableTables.jl中的类型)做处理, 包括filter, project, join, sort, group等, Query受C语言的LINQ和R语言的dplyr启发。

主要亮点

  • 支持几乎所有C#规范的查询表达式(LINQ), 还添加了额外的julia特有功能

  • 支持大量数据源:

    DataFrames.jl Pandas.jl IndexedTables.jl JuliaDB.jl TimeSeries.jl Temporal.jl CSVFiles.jl ExcelFiles.jl FeatherFiles.jl ParquetFiles.jl BedgraphFiles.jl StatFiles.jl DifferentialEquations(任何 DESolution) 数组和任何可以迭代的类型

  • 处理结果可以具现化为多种数据结构:

    迭代器 DataFrames.jl IndexedTables.jl JuliaDB.jl TimeSeries.jl Temporal.jl Pandas.jl StatsModels.jl CSVFiles.jl FeatherFiles.jl ExcelFiles.jl StatPlots.jl VegaLite.jl TableView.jl DataVoyager.jl 字典或任何数组

  • 一次处理可混合多个源的数据, 比如对DataFrameCSV文件进行join操作

  • 针对 DataFrames 的查询是完全类型稳定的

  • 提供三种API用来让包开发者使用Query.jl查询:

  • 最简单的API: 只需作者提供一个迭代器

  • 提供查询的完整图表示query graph

  • 提供自己的数据结构, 表示一个query graph

简介

Query.jl支持两种查询语法:

  • 独立查询操作(Standalone query operators)

  • LINQ样式的查询操作

独立查询操作

通过管道运算符组合成复杂的查询:

julia

using Query, DataFrames

df = DataFrame(name=["John", "Sally", "Kirk"],
               age=[23., 42., 59.],
               children=[3,5,2])

x = df |>
    @filter(_.age>50) |>
    @map({_.name, _.children}) |>
    DataFrame

println(x)

julia

LINQ样式查询

julia

q = @from <range variable> in <source> begin
    <query statements>
end

julia

多个查询条件用换行符分隔, 上述查询的LINQ样式为:

julia

x = @from i in df begin
    @where i.age>50
    @select {i.name, i.children}
    @collect DataFrame
    # 注意LINQ中用@collect 定义输出结果的格式
end
println(x)

julia

LINQ查询也可以管道, 使用@query(变量, 查询条件块):

julia

x = df |> @query(i, begin
            @where i.age>50
            @select {i.name, i.children}
        end) |> DataFrame
println(x)

julia

表和缺失值

  • 查询类似表的结构时, 数据被视为NamedTuple, Query中的{}语法用于方便构造NamedTuple

  • 缺失值被当作是DataValue类型(DataValues.jl)

    • 所有运算符和比较符都自动处理缺失

    • 如果使用的函数本身不支持缺失值的处理, 可以用.进行运算符提升

独立查询运算符

宏@map

source |> @map(element_selector)

element_selector必须是匿名函数, 接收单个元素

宏@filter

source |> @filter(filter_condition)

filter_condition必须是匿名函数, 返回Boolean类型

宏@groupby

简单形式:

source |> @groupby(key_selector)
key_selector是匿名函数, 为每个输入元素返回一个分组值

变体形式:

source |> @groupby(source, keyselector, elementselector)
element_selector: 匿名投影函数, 将元素分组之前应用该函数

@groupby通常和@map一起使用, 按照分组进行操作, 生成新的数据。

宏@orderby,@orderbydescending,@thenby,@thenbydescending

数据排序操作, 排序必须以@orderby或@orderby_descending开始, 后边可以接多个@thenby,@thenby_descending

source |> @orderby(key_selector)
key_selector:匿名函数, 根据返回的值进行排序

宏@groupjoin

outer |> @groupjoin(inner, outerselector, innerselector, result_selector)

outer|inner: 任何可查询的源 outer_selector|inner_selector: 匿名函数, 从指定源中提取值 result_selector: 匿名函数, 接受两个参数, 分别来自两个源

例子:

julia

df1 = DataFrame(a=[1,2,3], b=[1.,2.,3.])
df2 = DataFrame(c=[2,4,2], d=["John", "Jim", "Sally"])

x = df1 |> @groupjoin(df2, _.a, _.c, {t1=_.a, t2=length(__)}) |> DataFrame

println(x)

julia

宏@join

命令格式同@groupjoin

julia

df1 = DataFrame(a=[1,2,3], b=[1.,2.,3.])
df2 = DataFrame(c=[2,4,2], d=["John", "Jim","Sally"])

x = df1 |> @join(df2, _.a, _.c, {_.a, _.b, __.c, __.d}) |> DataFrame

println(x)

julia
`@join`和`@groupjoin`的区别是啥? groupjoin等于leftjoin后再group, join则只保留所有有交集的结果, 举个栗子:
julia

df1 = DataFrame(a=[1,2,3], b=[1.,2.,3.])
df2 = DataFrame(c=[2,4,2], d=["John", "Jim","Sally"])
x = df1 |> @groupjoin(df2, _.a, _.c, {A=_, B=__}) |> DataFrame # groupjoin
y = df1 |> @join(df2, _.a, _.c, {A=_, B=__}) |> DataFrame # join
julia> x.B # 输出结果跟df1的行数相同, 没有交集的行也输出了
  3-element Vector{Vector{NamedTuple{(:c, :d), Tuple{Int64, String}}}}:
   []
   [(c = 2, d = "John"), (c = 2, d = "Sally")]
   []
julia> y.B # 只输出了有交集的行, 且重复的记录逐行输出
  2-element Vector{NamedTuple{(:c, :d), Tuple{Int64, String}}}:
   (c = 2, d = "John")
   (c = 2, d = "Sally")

julia

宏@mapmany

source |> @mapmany(collectionselector, resultselector)
collection_selector: 匿名函数, 接受一个参数, 返回一个集合 result_selector: 匿名函数, 接受两个参数

例子:

julia

source = Dict(:a=>[1,2,3], :b=>[4,5])

q = source |> @mapmany(_.second, {Key=_.first, Value=__}) |> DataFrame

println(q)

julia

宏@take、@drop、@unique

source |> @take(n), n为整数
source |> @drop(n), n为整数
source |> @unique()

宏@select

选择指定列

source |> @select(selectors...)
  • source 可以是任何可以查询的来源。

  • selectors...的每个选择器都可以从source中选择元素并将其添加到结果集中,或者从结果集中选择元素并将其删除。

  • 选择器可以通过名称、位置或使用谓词函数来选择或删除元素。

julia

df = DataFrame(fruit=["Apple","Banana","Cherry"],
               amount=[2,6,1000],
               price=[1.2,2.0,0.4],
               isyellow=[false,true,false])

q1 = df |> @select(2:3, occursin("ui"), -:amount) |> DataFrame
# 2:3 => :amount, :price
# occursin("ui") => :amount, :price, :fruit
# -:amount => :price, :fruit
println(q1)

julia

宏@rename

source |> @rename(args...)

args...: 顺序执行的重命名操作(:raw => :new)

q = df |> @rename(:fruit => :food, :price => :cost, :food => :name) |> DataFrame

宏@mutate

source |> @mutate(args...)

args...: 指定元素名值转换公式, 顺序执行

julia

df = DataFrame(fruit=["Apple","Banana","Cherry"],
               amount=[2,6,1000],
               price=[1.2,2.0,0.4],
               isyellow=[false,true,false])

q = df |> @mutate(price = 2 * _.price + _.amount, isyellow = _.fruit == "Apple") |> DataFrame
println(q)

julia

宏@dropna、@dissallowna、@replacena

`@dropna`: 去除指定列包含缺失的行
source |> @dropna(columns...)

如果不带参数调用@dropna, 将删除任何一列包含NA(missing)的行。

`@dissallowna`: 将指定列的数据转换成不允许有缺失的类型
source |> @dissallowna(columns...)
`@replacena`: 替换指定列的缺失值为指定值 简单版:

source |> @replacena(replacement_value)
其中replacement_value是值, 简单版只适用于所有列都批量替换成同一个值的情形

完整版:

当不同的列需要不同的替换逻辑时, 要用完整版:

source |> @replacena(replacement_specifier...)

replacement_specifier...: 是column_name => replacement_value的键值对

LINQ风格的查询命令

julia

using Query, DataFrames

# Sorting:
# @orderby <attribute>[, <attribute>]
df = DataFrame(a=[2,1,1,2,1,3],b=[2,2,1,1,3,2])
x = @from i in df begin
    @orderby descending(i.a), i.b
    @select i
    @collect DataFrame
end

# Filtering
# @where <condition>
df = DataFrame(name=["John", "Sally", "Kirk"], age=[23., 42., 59.], children=[3,5,2])
x = @from i in df begin
    @where i.age > 30. && i.children > 2
    @select i
    @collect DataFrame
end

# Projecting
# @select <condition>
data = [1,2,3]
x = @from i in data begin
    @select i^2
    @collect
end
# query中应用`{}`可以将元素转换成命名元组
df = DataFrame(name=["John", "Sally", "Kirk"], age=[23., 42., 59.], children=[3,5,2])
x = @from i in df begin
    @select {i.name, Age=i.age} # 不定义名称时, 自动推断name; 显式声明名称Age;
    @collect DataFrame
end

# Flattening
# 使用多个@from实现:
# @from <range_var> in <selector>
source = Dict(:a=>[1,2,3], :b=>[4,5])
q = @from i in source begin
    @from j in i.second
    @select {Key=i.first,Value=j}
    @collect DataFrame
end

# Joining
# @join <range variable> in <source> on <left key> equals <right key>
df1 = DataFrame(a=[1,2,3], b=[1.,2.,3.])
df2 = DataFrame(c=[2,4,2], d=["John", "Jim","Sally"])

x = @from i in df1 begin
    @join j in df2 on i.a equals j.c
    @select {i.a,i.b,j.c,j.d}
    @collect DataFrame
end

# Group join
# @join <range variable> in <source> on <left key> equals <right key> into <group variable>
df1 = DataFrame(a=[1,2,3], b=[1.,2.,3.])
df2 = DataFrame(c=[2,4,2], d=["John", "Jim","Sally"])

x = @from i in df1 begin
    @join j in df2 on i.a equals j.c into k
    @select {t1=i.a,t2=length(k)}
    @collect DataFrame
end

# Left outer join
# @left_outer_join <range variable> in <source> on <left key> equals <right key>
source_df1 = DataFrame(a=[1,2,3], b=[1.,2.,3.])
source_df2 = DataFrame(c=[2,4,2], d=["John", "Jim","Sally"])

q = @from i in source_df1 begin
    @left_outer_join j in source_df2 on i.a equals j.c
    @select {i.a,i.b,j.c,j.d}
    @collect DataFrame
end

# Grouping
# @group <element selector> by <key selector> [into <range variable>]
df = DataFrame(name=["John", "Sally", "Kirk"], age=[23., 42., 59.], children=[3,2,2])
x = @from i in df begin
    @group i.name by i.children
    @collect
end
# with into:
df = DataFrame(name=["John", "Sally", "Kirk"], age=[23., 42., 59.], children=[3,2,2])
x = @from i in df begin
    @group i by i.children into g
    @select {Key=key(g),Count=length(g)}
    @collect DataFrame
end

# Split-Apply-Combine a.k.a dplyr
# @select new_var = agg_fun(g.var)
# agg_fun是聚合函数, 如mean, g是分组, var是要汇总的列
df = DataFrame(name=repeat(["John", "Sally", "Kirk"],
               inner=[1],outer=[2]), 
               age=vcat([10., 20., 30.],[10., 20., 30.].+3), 
               children=repeat([3,2,2],inner=[1],outer=[2]),
               state=[:a,:a,:a,:b,:b,:b])

x = @from i in df begin
    @group i by i.state into g
    @select {group=key(g),mage=mean(g.age), oldest=maximum(g.age), youngest=minimum(g.age)}
    @collect DataFrame
end

# Range variables
# @let <range variable> = <value selector>
df = DataFrame(name=["John", "Sally", "Kirk"],
               age=[23., 42., 59.], 
               children=[3,2,2])

x = @from i in df begin
    @let count = length(i.name)
    @let kids_per_year = i.children / i.age
    @where count > 4
    @select {Name=i.name, Count=count, KidsPerYear=kids_per_year}
    @collect DataFrame
end

julia

数据输出

@collect <TableType>
  • TableType: 可以是DataFrame,DataTable或者TypedTable

  • @collect不带参数则输出array类型

  • TableType可以是dict, 但只在输出结果是Pair的时候有效

  • TableType还可以是TimeArray,TS(Temporal.TS),IndexedTable, 具体略。

实验功能

可能会在未来版本中改动或消失的功能:

  • Source可以作为独立查询的第一个参数: source |> @map(_)@map(source,_)等价

  • 在独立查询命令中, _表示第一个参数, __表示第二个参数, 如果同时使用了___, query会自动创建带有两个参数的匿名函数:

julia

df_parents = DataFrame(Name=["John", "Sally"])
df_children = DataFrame(Name=["Bill", "Joe", "Mary"], Parent=["John", "John", "Sally"])

df_parents |> @join(df_children, _.Name, _.Parent, {Parent=_.Name, Child=__.Name}) |> DataFrame

julia
  • @unique的自定义选择器: source |> @unique(abs(_)) |> collect

VegaLite.jl

VegaLite VegaLite是基于json的数据可视化语法, 在这里有详细的介绍。VegaLite.jl是对VegaLite的封装, 旨在更方便地在julia中进行基于VegaLite的画图, 这里只介绍julia中的一些用法, VegaLite的语法不再赘述。

输入数据

  • DataFrame

  • JuliaDB

  • CSVFiles

  • VegaDatasets

VegaLite.js中没有的特性

  • 绘图结果在julia中保存为VLSpec类型的对象

  • @vlplot宏或者vl字符串宏创建VLSpec对象

  • 重载了load函数, 加载VagaLite的json文件为VLSpec

  • 重载了save函数, 用于输出VLSpec为图像:

julia

dataset("cars") |> 
    @vlplot(:bar, x="count()", y=:Origin) |>
    save("myplot.pdf")

julia

宏@vlplot

基本与VegaLite的json语法一致, 但是提供了一些简便的写法:

json

// VegaLite json语法
{
  "data": {
    "values": [
      {"a": "A","b": 28}, {"a": "B","b": 55}, {"a": "C","b": 43},
      {"a": "D","b": 91}, {"a": "E","b": 81}, {"a": "F","b": 53},
      {"a": "G","b": 19}, {"a": "H","b": 87}, {"a": "I","b": 52}
    ]
  },
  "mark": "bar",
  "encoding": {
    "x": {"field": "a", "type": "ordinal"},
    "y": {"field": "b", "type": "quantitative"}
  }
}

json
julia

# @vlplot语法
@vlplot(
    data={
        values=[
            {a="A",b=28},{a="B",b=55},{a="C",b=43},
            {a="D",b=91},{a="E",b=81},{a="F",b=53},
            {a="G",b=19},{a="H",b=87},{a="I",b=52}
        ]
    },
    mark="bar",
    encoding={
        x={field="a", type="ordinal"},
        y={field="b", type="quantitative"}
    }
)

julia
  • 移除了最外侧的{}

  • 冒号分隔改为用=键值对分隔;

  • 键不用引号, 左侧直接用字母, 右侧可以用symbol: mark=:bar

  • JSON中的null应该替换为Julia中的nothing

  • 提供与python的Altair中类似的编码速记用法

编码速记字符串语法

  • fieldtype的映射可以直接写成field:type:

julia

x={field=:a, type=:ordinal}
# 可以写成
x={"a:o"}

julia
注意: 字符串简写必须作为{}中的第一个位置参数出现
  • timeUnitaggregate中的聚合函数, 也可以用简写语法:

julia

# aggregate:
x={field=:foo, aggregate=:mean, type=:quantitative}
# 可以简写成:
x={"mean(foo)"} # 默认aggregate的结果就是quantitative

# timeUnit:
x={field=:foo, timeUnit=:year, type=:quantitative}
# 可以简写成:
x={"year(foo):t"}

julia
  • 如果字符串简写后不加其他属性,则可以省略{}: x="foo:q"等同于x={field=:foo, type=:quantitative}, 如果不想指定类型, 还可以直接用Symbol: x=:foo

  • encoding的简写

    • encoding可以写成enc

    • 更简单的, 可以把encoding的内容直接写在顶层: @vlplot(mark=:point, x="a:o", y=:b)

  • mark的简写

    • 用第一个位置参数表示mark: @vlplot(:point, x="a:o", y=:b)

    • {}指定更多标记属性, 将类型作为{}内第一个位置参数mark={:point, color=:red}

  • xy的简写

    • 可以作为@vlplot的第二个和第三个位置参数传递: @vlplot(:point, :colA, :colB)

vl字符串宏

spec = vl"""<raw Vega-Lite json>"""
  • Vega.printrepr可以将JSON转换成@vlplot格式的作图语法输出

VLSpec类型

VegaLite的属性可以作为VLSpec的对应属性进行访问:

julia

spec = data("cars") |> @vlplot(:point, x=:Acceleration, y=:Cylinders)
spec.mark
spec.encoding.x.field
# 使用Setfiled.jl/Accessors.jl的@set更改属性:
using Setfield  # imports `@set` etc.
spec2 = @set spec.mark = :line
spec3 = @set spec2.encoding.y.field = "Miles_per_Gallon"

julia

数据传入

  • 管道: df |> @filter(_.a>30) |> @vlplot(:point, x=:a, y=:b)

  • data关键字: @vlplot(:point, data=df, x=:a, y=:b)

  • 直接传递数据向量: @vlplot(:line, x=1:10, y={randn(10), title="XXX"})

  • 引用外部数据uri(基于URIParser.jl)和Path(基于FilePaths.jl):

julia

using FilePaths, URIParsing

p"xxx/yyy/zzz.csv" |> @vlplot(:point, :a, :b)
URI("https://www.xxx.com/yyy.csv") |> @vlplot(:point, :a, :b)

@vlplot(:point, data=p"subfolder/file.csv", x=:a, y=:b)
@vlplot(:point, data=URI("https://www.foo.com/bar.json"), x=:a, y=:b)

@vlplot(
    :point,
    data={
        url=p"subfolder/foo.txt",
        format={type=:csv}
    },
    x=:a,
    y=:b
)

julia

输出

julia

# 根据后缀名判断输出文件格式
p |> save("figure.png")
p |> save("figure.svg")
p |> save("figure.pdf")
p |> save("figure.eps")
p |> save("figure.vegalite")

save("figure.png", p)
save("figure.svg", p)
save("figure.pdf", p)
save("figure.eps", p)
save("figure.html", p)

julia

绘图技巧

  • 只将必要的列传递给@vlplot

DataVoyager.jl

将Julia的Data直接传递给Voyager进行交互式数据探索:

julia

using DataFrames, DataVoyager

data = DataFrame(a=rand(100), b=randn(100))

v1 = data |> Voyager()
v2 = Voyager(data)

# 用 `[]` 提取作图结果:
plot1 = v1[] # plot1 is a VLSpec
# 保存和重载:
v[] |> save("figure1.vegalite")
dataset("cars") |> load("figure1.vegalite")

julia